Skip to content

Myanglog

Java interface vs abstract class - 언제 무엇을 쓸까

2 min read

'인터페이스와 추상클래스의 차이'는 흔한 질문같지만 표면적인 차이 이상으로 들어가다보니 꽤 다양한 생각들을 만날 수 있었다.

Java8에서 인터페이스에 '디폴트 메서드'를 추가할 수 있게 되어서 둘의 차이점이 많지 않아졌다. 언제 무엇을 쓰는게 바람직한가를 좀더 자세히 살펴봤다.

특징

먼저 기본적인 특징들을 살펴보자.

인터페이스

  • 상수(static final ~) 와 추상메서드를 포함할 수 있다

    1interface Barkable {
    2 public static final int BLABLA_CONSTANT = 1;
    3 public abstract void bark();
    4}
  • Java8부터 디폴트 메서드 (안에 구현까지 작성 가능)를 포함할 수 있다

    • 디폴트 메서드: 인터페이스를 상속한 구현체에 공통으로 들어갈 코드를 디폴트 메서드에 작성하여 반복을 줄인다

      1// java.util.List에 있는 디폴트 메서드
      2
      3**default void replaceAll(UnaryOperator<E> operator) {
      4 Objects.requireNonNull(operator);
      5 final ListIterator<E> li = this.listIterator();
      6 while (li.hasNext()) {
      7 li.set(operator.apply(li.next()));
      8 }
      9}**
    • default method 제약사항

      • 구현체의 state를 참조할 수 없다
      • equalshashCode 같은 Object의 메서드 정의한건 디폴트 메서드로 제공해서는 안된다.
  • 스태틱 메서드를 가질 수 있다

    • 디폴트 메서드처럼 인터페이스에서 구현이 가능하다
      • 디폴트 메서드와 차이점은, overriding할 수 없다는 것
    • 스태틱 메서드이므로 클래스.메서드() 로 호출
  • 하나의 구현체가 여러개의 인터페이스를 구현(implement)할 수 있다

추상 클래스

  • abstract 키워드와 함께 선언한다.
    1abstract class Animal {
    2}
  • 추상 메서드를 가질 수 있다.
    • 추상 메서드는 구현부가 없는 메서드이다.
    • 추상 클래스에 추상 메서드가 반드시 필요하진 않지만, 추상메서드를 포함하는 클래스는 추상 클래스로 선언되어야 한다.
  • 그 자체로 인스턴스화 할 수 없다.
    • 인스턴스화하려면 추상 클래스를 상속한 클래스를 만들어야한다
  • 추상클래스는 생성자가 있고, state를 들고있을 수 있다.

Java8 interface의 default method

디폴트 메서드가 생긴 이유

  • stackoverflow답변들을 보다보니 'backward compatibility'~~ 란 말이 있었다.
    • 무슨말인가 보니 JDK 개발자들이 코드를 수정할 때 인터페이스 하나에 메서드를 추가하면 줄줄이 그것을 구현하고 있는 클래스들 코드가 깨져서, 호환성을 유지하기 위해 디폴트 메서드를 만들었다고 한다.
      • 이미 있는 인터페이스에 새로운 메서드를 추가할 때 인터페이스 안에 구현이 있게되면 구현 클래스들에 영향을 받지 않기 때문.
      • 대표적인 예로 collection 인터페이스에 forEach 메서드가 디폴트 메서드로 추가되었는데, 이렇게 해서 기존에 있던 인터페이스의 파라미터에 lambda expression을 넣을 수 있게 되었다.
        1public interface Iterable<T> {
        2 public default void forEach(Consumer<? super T> consumer) {
        3 for (T t : this) {
        4 consumer.accept(t);
        5 }
        6 }
        7}
      • (디폴트 메서드를 처음부터 의도가 있어서 만들었다기보다 개발하다보니 필요해져서(?) 만든 것에 가까워서 'backward'라는 표현을 쓴 것 같다.)

언제 무엇을 쓸까

추상클래스를 쓰는 것이 좋은 경우

  • 명확하게 계층구조가 필요하여 상속관계로 만들고, 공통된 기능 구현이 필요할 때
  • 코드 예시를 든 좋은 글이 있어서 간단히 요약:
    • 대용량 SMS sender를 구현 - 여러 나라의 통신사들이 다른 tower를 갖고 있어서 각각 다른 구현이 필요 + 공통으로 지켜야할 규칙(DoNotDisturb모드인지 확인) 도 있는 상황일 때

    • SMS를 보내는 추상화된 코드:

      1public void sendSMS(){
      2 establishConnectionWithYourTower();
      3 checkIfDoNotDisturbMode();
      4 // -- SMS 보내기 --
      5 destroyConnectionWithYourTower();
      6}
      7
      8public void establishConnectionWithYourTower(){
      9 // 통신사마다 다르다
      10}
      11
      12public void checkIfDoNotDisturbMode(){
      13}
      14
      15public void destroyConnectionWithYourTower(){
      16 // 통신사마다 다르다
      17}
    • 구현

      1abstract class SMSSender{
      2
      3 abstract public void establishConnectionWithYourTower();
      4
      5 public void sendSMS(){
      6 establishConnectionWithYourTower();
      7 checkIfDoNotDisturbMode();
      8 // -- SMS 보내기 --
      9 destroyConnectionWithYourTower();
      10 }
      11
      12 abstract public void destroyConnectionWithYourTower();
      13
      14 public void checkIfDoNotDisturbMode(){
      15 // 추상클래스 안에서 구현
      16 }
      17}
      18
      19/* SMSSender를 통신사 클래스들이 상속 */
      20class Vodafone extends SMSSender{
      21 @Override
      22 public void establishConnectionWithYourTower() {
      23 // Vodafone 방식으로 커넥션 맺기
      24 }
      25
      26 @Override
      27 public void destroyConnectionWithYourTower() {
      28 // Vodafone 방식으로 커넥션 종료
      29 }
      30}
      31
      32class Airtel extends SMSSender{
      33 @Override
      34 public void establishConnectionWithYourTower() {
      35 }
      36
      37 @Override
      38 public void destroyConnectionWithYourTower() {
      39 }
      40}

인터페이스를 쓰는 것이 좋은 경우

책 '이펙티브 자바'에서는 '추상클래스보다 인터페이스를 우선하라'고 하며 여러 이유와 예시 케이스들을 든다.

  1. 기존 클래스에 새로운 인터페이스 구현이 쉽다
    • 인터페이스가 요구하는 메서드들을 추가하고, implements하면 끝.
    • 추상클래스로는 단일 상속만 가능하므로, 새로 추상클래스를 끼워 넣으면 적절하지 않은 상황에서 서브클래스들이 추상클래스를 상속받는다.
  2. mixin 정의 시 적절하다
    • 클래스의 원래의 주된 타입 외에 특정한 다른 선택적 기능을 'mix in'하여 제공한다고 선언하는 효과를 줄 수 있다.
    • ex. 어떤 클래스가 Comparable 을 구현하면, Comparable 을 구현한 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 것.
  3. 계층구조가 없는 타입 프레임워크를 만들 수 있다
    • 계층을 엄격하게 구분하기 어려운 개념들도 있다
      • ex. Singer, Songwriter - 둘 다를 구현하는 클래스가 필요할 수 있다. 이걸 인터페이스를 쓰지 않는다면 조합마다 매번 새로운 abstract클래스를 사용하게 됨. 이런 계층이 쌓이고 쌓이면 조합 폭발...
        1// interface
        2public interface SingerSongWriter extends Singer, SongWriter {
        3 void actSensitive();
        4}
        5
        6// abstract class
        7public abstract class SingerSongWriter {
        8 abstract AudioClip sing(Song s);
        9 abstract Song compose(int chartPosition);
        10 void actSensitive();
        11}

인터페이스와 추상클래스의 장점을 모두 취하는 방법

⇒ 인터페이스와 추상 골격 구현 (skeletal implementation) 클래스를 함께 제공하기

  • 인터페이스로는 타입을 정의하고, 필요 시 디폴트 메서드 제공
  • 골격 구현 클래스는 나머지 메서드들 구현
  • ⇒ 디자인패턴 중 '템플릿 메서드 패턴'을 따르는 방법이다.
  • ex 1) Java Collection framework의 AbstractList, AbstractMap, etc.
    • interface로 Map이 있고 골격구현 클래스로 AbstractMap이 있다.
    • HashMap, TreeMap 등은 AbstractMap을 상속하지만 SortedMap은 AbstractMap을 상속하지 않고 Map을 구현한다.
  • ex 2) 조영호 - '오브젝트' 책에도 추상클래스를 이 방식으로 리팩토링하는 과정이 나온다. 글이 너무 길어질 듯 하여 잘 정리된 링크 참조 :-)
  • 이 방법의 장점 : 추상클래스처럼 구현을 도와주면서 + 추상클래스로 타입을 정의할 때 생기는 제약에서 자유롭다.
  • 다만 현실적으로 인터페이스를 추가하는 것이 과할 때가 있다. 트레이드오프이므로 상황에 맞게!

References